package software.lib.server;

import com.sun.istack.internal.NotNull;
import javafx.util.Pair;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.*;
import org.jboss.netty.handler.codec.http.*;
import org.jboss.netty.handler.stream.ChunkedStream;
import org.jboss.netty.util.CharsetUtil;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;

/**
 * Created by Wang on 2017/5/9.
 */
public class ServerChannelHandler extends SimpleChannelHandler {

    public ServerChannelHandler(@NotNull Map<String, Servlet> servletMap, @NotNull List<Pair<String, Filter>> fliterList) {
        Map<String, List<String>> partitionMap = servletMap.keySet().<String>stream().collect(urlMapCollector);

        List<String> defaultKeys = partitionMap.get("default");
        if (defaultKeys != null && !defaultKeys.isEmpty()) {
            defaultServlet = servletMap.get(defaultKeys.get(0));
        }

        List<String> specificKeys = partitionMap.get("specific");
        specificMap = new TreeMap<>();
        if (specificKeys != null && !specificKeys.isEmpty()) {
            specificKeys.stream().forEach(
                    x -> specificMap.put(x, servletMap.get(x))
            );
        }

        List<String> pathKeys = partitionMap.get("path");
        pathMap = new TreeMap<>();
        if (pathKeys != null && !pathKeys.isEmpty()) {
            pathKeys.stream().forEach(
                    x -> pathMap.put(x, servletMap.get(x))
            );
        }

        List<String> extKeys = partitionMap.get("extend");
        pathMap = new TreeMap<>();
        if (extKeys != null && !extKeys.isEmpty()) {
            extKeys.stream().forEach(
                    x -> extMap.put(x, servletMap.get(x))
            );
        }

        this.fliterList = fliterList;
    }

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        DefaultHttpRequest defaultHttpRequest = (DefaultHttpRequest) e.getMessage();

        String urlpath = defaultHttpRequest.getUri();
        Servlet matchedServlet = matchUrl(urlpath);

        if (matchedServlet != null) {
            HttpServletRequest request = adapt(defaultHttpRequest, matchedServlet);
            MockHttpServletResponse response = new MockHttpServletResponse();

            Optional<MyFliterChain> myFliterChainOptional =
                    fliterList.stream().filter(x -> {
                        String key = x.getKey();
                        switch (classifyUrlPattern(urlpath)) {
                            case "specific":
                                return urlpath.equals(key.equals(urlpath));
                            case "path":
                                return urlpath.startsWith(key.replace("*", ""));
                            case "extend":
                                return urlpath.endsWith(key.replace("*", ""));
                            default:
                                return true;
                        }
                    }).map(x -> x.getValue())
                            .<MyFliterChain>map(filter -> new MyFliterChain(filter, matchedServlet))
                            .reduce(MyFliterChain::setNext);

            if (myFliterChainOptional.isPresent())
                myFliterChainOptional.get().doFilter(request, response);
            else
                matchedServlet.service(request, response);


            HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(response.getStatus());
            HttpResponse defaultHttpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, responseStatus);

            HttpHeaders responseHeaders = defaultHttpResponse.headers();
            for (String headname : response.getHeaderNames()) {
                for (String value : response.getHeaders(headname)) {
                    responseHeaders.add(headname, value);
                }
            }

            Channel ch = e.getChannel();
            ch.write(defaultHttpResponse);

            InputStream contentStream = new ByteArrayInputStream(response.getContentAsByteArray());
            ChannelFuture writeFuture = ch.write(new ChunkedStream(contentStream));
            writeFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
        e.getCause().printStackTrace();
        Channel ch = e.getChannel();
        if (ch.isConnected()) {
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
        ch.close();
    }

    private Servlet matchUrl(@NotNull String urlpath) {
        Optional<String> specificUrl = specificMap.keySet().parallelStream()
                .filter(x -> x.equals(urlpath)).findFirst();
        if (specificUrl.isPresent()) {
            return specificMap.get(specificUrl.get());
        }

        Optional<String> pathUrl = pathMap.keySet().parallelStream()
                .map(x -> x.replace("*", ""))
                .filter(x -> urlpath.startsWith(x)).sequential()
                .sorted(Comparator.comparing(String::length).reversed())
                .findFirst();
        if (pathUrl.isPresent()) {
            return pathMap.get(pathUrl.get());
        }

        Optional<String> extUrl = extMap.keySet().parallelStream()
                .map(x -> x.replace("*", ""))
                .filter(x -> urlpath.endsWith(x))
                .findAny();

        if (extUrl.isPresent()) {
            return extMap.get(extUrl.get());
        }

        return defaultServlet;
    }

    /**
     * Adapt org.jboss.netty.handler.codec.http.HttpRequest to javax.servlet.http.HttpServletRequest
     *
     * @param request org.jboss.netty.handler.codec.http.HttpRequest
     * @return javax.servlet.http.HttpServletRequest
     */
    protected HttpServletRequest adapt(@NotNull HttpRequest request, @NotNull Servlet servlet) {
        MockHttpServletRequest servletRequest = new MockHttpServletRequest(servlet.getServletConfig().getServletContext());
        servletRequest.setRequestURI(request.getUri());
        servletRequest.setPathInfo(request.getUri());
        servletRequest.setMethod(request.getMethod().getName());

        UriComponents uriComponents = UriComponentsBuilder.fromUriString(request.getUri()).build();
        if (uriComponents.getScheme() != null) {
            servletRequest.setScheme(uriComponents.getScheme());
        }
        if (uriComponents.getHost() != null) {
            servletRequest.setServerName(uriComponents.getHost());
        }
        if (uriComponents.getPort() != -1) {
            servletRequest.setServerPort(uriComponents.getPort());
        }

        HttpHeaders heads = request.headers();
        heads.names();
        for (String name : heads.names()) {
            for (String value : heads.getAll(name)) {
                servletRequest.addHeader(name, value);
            }
        }

        servletRequest.setContent(request.getContent().array());

        try {
            if (uriComponents.getQuery() != null) {
                String query = UriUtils.decode(uriComponents.getQuery(), "UTF-8");
                servletRequest.setQueryString(query);
            }

            for (Map.Entry<String, List<String>> entry : uriComponents.getQueryParams().entrySet()) {
                for (String value : entry.getValue()) {
                    servletRequest.addParameter(
                            UriUtils.decode(entry.getKey(), "UTF-8"),
                            UriUtils.decode(value, "UTF-8"));
                }
            }
        } catch (UnsupportedEncodingException ex) {
            // shouldn't happen
        }

        return servletRequest;
    }

    private static String classifyUrlPattern(String x) {
        if (x.contains("*")) {
            if (x.startsWith("/")) {
                return "path";
            } else {
                return "extend";
            }
        } else {
            if (x.equals("/"))
                return "default";
            else {
                return "specific";
            }
        }
    }

    protected static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");
        response.setContent(ChannelBuffers.copiedBuffer(
                "Failure: " + status.toString() + "\r\n",
                CharsetUtil.UTF_8));

        // Close the connection as soon as the error message is sent.
        ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
    }

    private final Collector<String, ?, Map<String, List<String>>> urlMapCollector = Collectors.groupingBy(ServerChannelHandler::classifyUrlPattern);
    /**
     * 默认的 Servlet  匹配 /
     */
    protected Servlet defaultServlet;
    /**
     * 精确匹配
     */
    protected Map<String, Servlet> specificMap;
    /**
     * 路径匹配
     */
    protected Map<String, Servlet> pathMap;
    /**
     * 扩展名匹配
     */
    protected Map<String, Servlet> extMap;
    /**
     * 过滤器列表
     */
    protected List<Pair<String, javax.servlet.Filter>> fliterList;

    private static class MyFliterChain implements FilterChain {
        public MyFliterChain(Filter fliter, Servlet targetServlet) {
            this.fliter = fliter;
            this.targetServlet = targetServlet;
        }

        public MyFliterChain setNext(MyFliterChain next) {
            this.next = next;
            return this;
        }
        javax.servlet.Filter fliter;
        Servlet targetServlet;
        MyFliterChain next;

        @Override
        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
            if (next != null)
                fliter.doFilter(request, response, next);
            else
                fliter.doFilter(request, response, new FilterChain() {
                    @Override
                    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
                        targetServlet.service(request, response);
                    }
                });
        }


    }
}


